package org.helsoft.mojo;

import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
import org.helsoft.mojo.api.AbstractPDEMojo;
import org.helsoft.mojo.api.Bundle;
import org.helsoft.mojo.api.BundleFileFilter;
import org.helsoft.mojo.api.PluginDescriptor;
import org.helsoft.mojo.api.Version;

/**
 * 
 * @author hesteguet
 * 
 * @goal compile
 */
public class CompileMojo extends AbstractPDEMojo {
	/**
	 * The location of the workspace where this plugin is located.
	 * 
	 * @parameter expression="${workspaceLocation}"
	 *            default-value="${project.basedir}/.."
	 * @required
	 */
	private String workspaceLocation;

	/**
	 * 
	 */
	private final List<File> resolvedBundles = new ArrayList<File>();

	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
		if (getEclipseLocation() == null) {
			fail("An eclipse location should be specified");
		}

		File eclipseDir = getEclipseLocation();
		if (!eclipseDir.isDirectory()) {
			fail("'" + eclipseDir
					+ "' it's not a valid eclipse installation directory");
		}

		File pluginsDir = new File(eclipseDir, "plugins");
		if (!pluginsDir.isDirectory()) {
			fail("'" + eclipseDir
					+ "' it's not a valid eclipse installation directory : "
					+ "missing plugins directory");
		}
		
		MavenProject project = getProject().clone();
		project.setFile(getPDEPom());		
		
		//We add eclipse dependencies
		createAllDependencies(project, getPluginDescriptor(), pluginsDir);

		//We compile
		doCompile(project);		
	}

	/**
	 * @param project
	 * @throws MojoFailureException
	 * @throws MojoExecutionException
	 */
	private void doCompile(MavenProject project) throws MojoFailureException, MojoExecutionException {
		// TODO Auto-generated method stub
		try {
			writeModel(project.getModel(), project.getFile());
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		MavenExecutionRequest request = createExecutionRequest();
		request.setPom(project.getFile());
		request.setGoals(Arrays.asList("compiler:compile"));
		
		MavenExecutionResult result = execute(request);
		if (result.hasExceptions()) {
			Throwable t = result.getExceptions().get(0);
			if (t instanceof MojoFailureException) {
				throw (MojoFailureException) t;
			} else if (t instanceof MojoExecutionException) {
				throw (MojoExecutionException) t;
			} else {
				fail(t.getMessage(), t);
			}
		} 
	}

	/**
	 * @param pluginDir
	 * @param bundle
	 * @return
	 * @throws MojoExecutionException
	 * @throws MojoFailureException
	 */
	private File resolvePluginFrom(File pluginDir, Bundle bundle)
			throws MojoExecutionException, MojoFailureException {
		File result = null;

		File[] files = pluginDir.listFiles(new BundleFileFilter(bundle));
		if (files.length == 0) {
			// We take any version of the bundle
			files = pluginDir.listFiles(new BundleFileFilter(bundle, true));
		}

		if (files.length > 0) {
			result = files[files.length - 1];
		}

		return result;
	}

	/**
	 * @param bundle
	 * @return
	 */
	private File resolvePluginFromWorkspace(Bundle bundle) {
		File result = null;

		File dir = workspaceLocation == null ? null : new File(
				workspaceLocation);
		if (dir != null && dir.isDirectory()
				&& new File(dir, ".metadata").isDirectory()) {
			final String name = bundle.getFullName();
			if (name != null) {
				File[] files = new File(workspaceLocation)
						.listFiles(new FileFilter() {
							@Override
							public boolean accept(File pathname) {
								return name.equals(pathname.getName());
							}
						});

				if (files.length == 1) {
					// The first step is to synchronize the manifest with the
					// bundle infos
					PluginDescriptor desc = null;
					try {
						desc = PluginDescriptor.fromFile(files[0]);
						// We try to replace the version of the bundle with the
						// one of the plugin
						String bundleVersion = desc.getBundleVersion();
						if (bundleVersion != null
								&& !bundleVersion.trim().isEmpty()) {
							bundle.setVersion(bundleVersion);
						}
					} catch (IOException e) {
						// Ignore
					}
				}

			}
		}

		return result;
	}

	/**
	 * @param project
	 * @param pluginDir
	 * @param pluginLocation
	 * @return
	 * @throws MojoExecutionException
	 * @throws MojoFailureException
	 */
	private File[] resolvePluginDependencies(MavenProject project, File pluginDir, File pluginLocation)
			throws MojoExecutionException, MojoFailureException {
		File[] result = {};

		PluginDescriptor descriptor = null;
		try {
			descriptor = PluginDescriptor.fromFile(pluginLocation);
		} catch (Exception e) {
			warn("Error while getting manifest of " + pluginLocation,
					e);
		}

		// We resolve the manifest dependencies
		if (descriptor != null) {
			createAllDependencies(project, descriptor, pluginDir);
		} else {
			warn("No manifest found for '%s'", pluginLocation);
		}

		return result;
	}

	/**
	 * @param project
	 * @param bundle
	 * @param path
	 */
	private void createDependency(MavenProject project, Bundle bundle, File path) {
		String artifactId = bundle.getArtifactId();
		String groupId = bundle.getGroupId();
		String scope = null;
		String systemPath = null;
		String type = null;
		String version = null;

		// resolve the version
		if (!bundle.getVersions().isEmpty()) {
			version = bundle.getVersions().get(bundle.getVersions().size() - 1)
					.toString();
		} else if (path != null) {
			// we try to extract the version from the file name
			Version vrs = Version.fromFileName(path.getName());
			if (vrs != null) {
				version = vrs.toString();
			}
		}

		if (version == null) {
			version = "1.0";
		}

		// The scope and the system path
		if (path != null) {
			scope = Artifact.SCOPE_SYSTEM;

			// The dependencies is a local file
			if (path.isDirectory()) {
				// we are looking for jar files
				File[] files = path.listFiles(new FileFilter() {
					@Override
					public boolean accept(File file) {
						return file.isFile()
								&& file.getName().toLowerCase()
										.endsWith(".jar");
					}
				});
				// The easy case the folder contains only one jar
				if (files.length == 1) {
					String ext = extOf(files[0].getName()).trim();
					if (!ext.isEmpty()) {
						type = ext;
					}
					systemPath = files[0].getPath();
				} else if (files.length > 1) {
					// The folder has multiple jars. We declare each one
					// as a dependency
					for (File jar : files) {
						Dependency dep = new Dependency();
						dep.setGroupId(bundle.getFullName());
						dep.setArtifactId(jar.getName().substring(0,
								jar.getName().lastIndexOf(".")));
						if (version != null) {
							dep.setVersion(version);
						}
						dep.setScope(Artifact.SCOPE_SYSTEM);
						dep.setType("jar");
						dep.setSystemPath(jar.getPath());
						registerDependency(project, dep);
					}
				} else {
					// The bundle dependency could not been resolved
					warn("Unable to resolve dependency " + bundle);
					return;
				}
			} else {
				String ext = extOf(path.getName()).trim();
				if (!ext.isEmpty()) {
					type = ext;
				}
				systemPath = path.getPath();
			}
		} else {
			scope = Artifact.SCOPE_COMPILE;
		}

		Dependency dep = new Dependency();
		dep.setArtifactId(artifactId);
		dep.setGroupId(groupId);
		dep.setScope(scope);
		dep.setSystemPath(systemPath);
		dep.setType(type);
		dep.setVersion(version);

		// record the dependency
		registerDependency(project, dep);
	}

	/**
	 * @param project
	 * @param dep
	 */
	private void registerDependency(MavenProject project, Dependency dep) {
		/**
		 * We try to only add resolvable dependencies.
		 * Which means the ones without a system scope or with a valid systemPath.
		 */
		if (dep != null
				&& (!Artifact.SCOPE_SYSTEM.equals(dep.getScope()) || (dep
						.getSystemPath() != null && new File(
						dep.getSystemPath()).isFile()))) {
			project.getModel().addDependency(dep);
		}
	}

	/**
	 * @param project
	 * @param descriptor
	 * @param pluginsDir
	 * @throws MojoExecutionException
	 * @throws MojoFailureException
	 */
	private void createAllDependencies(MavenProject project, PluginDescriptor descriptor,
			File pluginsDir) throws MojoExecutionException,
			MojoFailureException {
		for (Bundle bundle : descriptor.getBundles()) {
			File file = resolvePluginFrom(pluginsDir, bundle);
			if (file == null) {
				file = resolvePluginFromWorkspace(bundle);
			}
			if (file != null && !resolvedBundles.contains(file)) {
				debug("Resolved dependency for %s = %s", bundle, file);
				resolvedBundles.add(file);
				try {
					file = fetch(file);
				} catch (IOException e) {
					// Ignore
				}
				createDependency(project, bundle, file);
				resolvePluginDependencies(project, pluginsDir, file);
			} else if (file == null) {
				warn("Unable to resolve dependency %s", bundle);
			}

			// We handle the special case of SWT bundle
			if ("org.eclipse.swt".equalsIgnoreCase(bundle.getFullName())) {
				addSWTCompanions(project, bundle, pluginsDir);
			}
		}
	}

	/**
	 * Copy the file into the temporary libraries location if one is set.
	 * 
	 * @param file
	 *            The file
	 */
	private File fetch(File file) throws IOException {
		File dir = getLibrairiesLocation();
		File newLocation = file;
		if (file != null && file.isFile() && dir != null
				&& (dir.isDirectory() || dir.mkdirs())) {
			newLocation = new File(dir, file.getName());
			FileUtils.copyFileIfModified(file, newLocation);
		}
		return newLocation;
	}

	/**
	 * @param str
	 * @return
	 */
	private String extOf(String str) {
		String result = "";
		int idx = str.lastIndexOf(".");
		if (idx > -1) {
			result = str.substring(idx + 1);
		}
		return result;
	}

	/**
	 * SWT comes with a system specific library companion not declared into its
	 * manifest. So to build eclipse application which use SWT We must add this
	 * libraries into the classpath.
	 * 
	 * @param swtBundle
	 *            The bundle
	 * @param pluginDir
	 *            The plugins directory
	 * @return
	 */
	private void addSWTCompanions(MavenProject project, final Bundle swtBundle, File pluginDir) {
		File[] files = pluginDir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				return name.startsWith(swtBundle.getFullName() + ".")
						&& name.endsWith("jar") && !name.contains("source");
			}
		});
		for (File file : files) {
			createDependency(project, Bundle.fromFileName(file.getName()), file);
		}
	}

}
